package com.didd.core.redislock;

import com.didd.core.common.base.ResultCode;
import com.didd.core.common.base.RpcResponse;
import com.didd.core.exception.RedisLockException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Redis Lock Manager
 * redis锁管理
 *
 * @author diddd
 * @Created 2020/5/15 16:16
 */
@Slf4j
public class RedisLockManager {
    private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1] ) else return 0 end";

    private StringRedisTemplate stringRedisTemplate;

    public RedisLockManager(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * @param lockType    锁类型
     * @param requestId   业务请求ID
     * @param isTry       是否重试
     * @param acquireTime 获得锁超时时间
     * @param expireTime  锁过期时间
     * @param isThrowable 是否抛出异常
     * @return RpcResponse<Void>
     */
    public RpcResponse<Void> holdLock(String lockType, String requestId, boolean isTry, long acquireTime, long expireTime, boolean isThrowable) {
        log.info("[diddd-common][RedisLock-Hold] lockType:[{}],requestId:[{}]", lockType, requestId);
        boolean holdRedisLock = this.holdRedisLock(lockType, requestId, isTry, acquireTime, expireTime);
        if (!holdRedisLock) {
            log.info("[diddd-common][RedisLock-Hold] redis 分布式加锁失败:lockType:[{}];requestId:[{}]", lockType, requestId);
            if (isThrowable) {
                throw new RedisLockException(ResultCode.REDIS_LOCK_WAIT);
            }
            return RpcResponse.fail(ResultCode.REDIS_LOCK_WAIT);
        }
        return RpcResponse.success();
    }

    /**
     * @param lockType    锁类型
     * @param requestId   业务请求ID
     * @param isThrowable 是否抛出异常
     * @return RpcResponse<Void>
     */
    public void releaseLock(String lockType, String requestId, boolean isThrowable) {
        //释放锁
        log.info("[diddd-common][RedisLock-Release] lockType:[{}],requestId:[{}]", lockType, requestId);
        boolean releaseRedisLock = this.releaseRedisLock(lockType, requestId);
        if (!releaseRedisLock) {
            log.info("[diddd-common][RedisLock-Hold] redis 分布式释放锁失败:lockType:[{}];requestId:[{}]", lockType, requestId);
        }
    }

    /**
     * 分布式锁
     *
     * @param lockType     锁类型
     * @param requestId   业务请求ID
     * @param isTry        是否重试
     * @param acquireTime  有效期 单位:秒
     * @param expireTime   加锁过期时间 单位:秒
     * @return boolean
     */
    private boolean holdRedisLock(String lockType, String requestId, boolean isTry, long acquireTime, long expireTime) {
        long nonaTime = System.currentTimeMillis();
        while (true) {
            try {
                RedisCallback<String> callback = (connection) -> {
                    JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                    //EX = seconds; PX = milliseconds
                    return commands.set(lockType, requestId, "NX", "EX", expireTime);
                };
                String result = stringRedisTemplate.execute(callback);
                if (!StringUtils.isEmpty(result)) {
                    return true;
                }
                if (acquireTime != 0L && System.currentTimeMillis() - nonaTime > acquireTime * 1000L) {
                    return false;
                }
                if (isTry) {
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException var11) {
                        return false;
                    }
                } else {
                    return false;
                }
            } catch (Exception e) {
                log.error("set blockLock occured an exception", e);
                return false;
            }
        }

    }

    /**
     * 分布式解锁
     *
     * @param lockType     锁类型
     * @param requestId   业务请求ID
     * @return
     */
    private boolean releaseRedisLock(String lockType, String requestId) {
        // 释放锁的时候，有可能因为持锁之后方法执行时间大于锁的有效期，此时有可能已经被另外一个线程持有锁，所以不能直接删除
        try {
            List<String> keys = new ArrayList<>();
            keys.add(lockType);
            List<String> args = new ArrayList<>();
            args.add(requestId);

            // 使用lua脚本删除redis中匹配value的key，可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中，集群模式直接抛出不支持执行脚本的异常，所以只能拿到原redis的connection来执行脚本
            RedisCallback<Long> callback = (connection) -> {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单机模式虽然执行脚本的方法一样，但是没有共同的接口，所以只能分开执行
                // 集群模式
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
                }

                // 单机模式
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
                }
                return 0L;
            };
            Long result = stringRedisTemplate.execute(callback);

            return result != null && result > 0;
        } catch (Exception e) {
            log.error("release lock occured an exception", e);
        }
        return false;
    }


    public static void main(String[] args) {
        for(int i = 0;i<10000;i++){
            System.out.println(UUID.randomUUID().toString());
        }
    }
}